前端项目中合理使用Babel
相信Web前端开发的小伙伴们,对Babel都不陌生了。几乎现代的稍微大一点儿的前端项目,都会用到它来帮我们编译JS(或TS)代码。 虽然经常接触,但是可能了解的并深入,有很多似懂非懂的地方。我们这里简要介绍一下有关前端项目引入Babel的注意点。
Babel 是啥?
按照Babel官方的说法,Babel
是一个JavaScript编译器。就是将现代的ECMAScript(或typescript)转成能支持特定宿主环境运行的 Javascript语言。 现在是 2023 年,ES的标准已经推行到了ES2023了,每年都会有很多新提案加入到ES的语言特性中,让 JavaScript 成为了一门支撑开发大型前端项目的通用语言。
ECMAScript 是 JavaScript的语言规范,JavaScript是某一个平台(或环境)的实现。两者就是 普通话 和 方言的关系。
由于JavaScript主要是运行浏览器中的,浏览器安装在不同的设备上。在现在面临的问题是,还存在不少旧设备以及一些老的浏览器,它们不支持新的 ES语言特性,我们的产品又不能放弃这一部分的用户群体。 只能选择让项目代码尽可能地兼容老的设备或浏览器。那如何既能使用新的语言特性提升开放体验,又能兼顾老旧的浏览器和设备。
这时候Babel 就派上用场了。我们在开发环境下继续使用高级的ES语法特性,在打包到生产环境中的代码经过 Babel 编译,输出到特定平台所支持的 JS 版本。
再比如,我们在平时开发中写了React组件、Vue组件是不能直接在浏览器中运行的,需要进一步编译。
Babel的运行原理
Babel的实现过程,大致经过解析
,转换
,生成
这么三个阶段。
- 扫描项目工程中的源代码目录,获得源码的输入
- 将源代码,解析成AST(抽象语法树)
- 根据需求,将解析得到的AST进一步加工转换,生成满足特定环境下的AST'
- 将转换后的AST,生成目标代码,一般是输出到本地磁盘上的文件。
需要注意的是,Babel只进行语言转换(比如const声明的变量、箭头函数等),而不提供新的语言API的支持(比如promise、Map、Set等),需要使用相关的插件来满足需求。
而Babel的架构模式,正式基于插件的架构模式。将拓展的功能,从核心模块中抽离出来,形成一系列的插件。 既降低了框架的复杂度,也提升了架构的灵活程度。核心模块core包 和 插件 plugin 包 ,以松散的方式耦合,两者在功能不变的情况下,可以独立发布,互相补充影响。并且公开了插件的接口,让开发者也能对插件进行扩展。
Babel的插件
大致分为两类:语法插件(Syntax Plugins) 和 转换插件。
- 语法插件:解析特定类型的语法。它们能够帮助构造抽象语法树(AST)。典型的语法插件有:syntax-async-functions 以及 syntax-jsx
- 转换插件:其作用为修改抽象语法树。典型的转换插件有:transform-async-to-generator、transform-react-jsx、transform-es2015-arrow-functions 等
presets 预设
babel中的 presets 是一些列Babel插件的集合。因为Babel的插件众多,使用的时候一个个配置,太过繁琐了。 有了presets 方便用户对插件的使用和管理。
Babel 官方为我们提供了一些常见的 preset
- @babel/preset-env:转换、编译 ES2015+ 的语法插件集;
- @babel/preset-flow:为 Flow 提供的预设,包含所有 flow 相关的插件;
- @babel/preset-react:为 React 提供的预设,包含所有 React 相关的插件;
- @babel/preset-typescript:为 TS 提供的预设,包含所有 TS 相关的插件;
我们平时使用到的是 @babel/preset-env ,这个预设会随着 ECMA 规范的更新增加自身的内容。那么这样的问题就是,随着时间的推移,该预设的内容会越来越多,编译的速度也会越来越慢。并且随着时间的推移,冗余的插件也会越来越多。所以,目前 Babel 官方不再推出 babel-preset-es2017 以后的预设了。
如果使用默认配置,那么就会和 babel-preset-latest 预设相同,会加载从 ES2015 开始的所有 preset。如果我们想根据业务项目的需求,设定运行的环境,来定制输出的代码。可以传入target 参数,例如,支持最近的 3个浏览器版本和 安卓4.4 版本以及 iOS 9.0 以上版本运行我们的代码,那么可以按照如下配置:
presets: [
[
'@babel/preset-env',
{
targets: {
"browsers": [
"last 3 versions",
"Android >= 4.4",
"iOS >= 9.0"
],
}
},
],
]
presets: [
[
'@babel/preset-env',
{
targets: {
"browsers": [
"last 3 versions",
"Android >= 4.4",
"iOS >= 9.0"
],
}
},
],
]
搭配 Polyfill 使用
由于 Babel 默认在编译时只会转换新的 JavaScript 语法(syntax),但不会转换 API,比如 Set、Maps、Generator、Proxy、Promise 等全局对象,以及一些定义在全局对象上的方法(比如Array.from、Object.assign)都不会被转译。因此 Babel 官方推出了 @babel/polyfill 库。
其核心依赖是 core-js@2 和 regenerater-runtime/runtime。core-js 是 JS 标准库的 polyfill,为其提供垫片能力,regenerater-runtime/runtime 用来转译 generators 和 async 函数。
core-js 提供对各种ES6+ API的 polyfill
regenerater-runtime/runtime 转译 generators 和 async 函数
core-js 是一个 JavaScript 标准库,里面包含了 ESCAScreipt 2020 在内的多项特性的 polyfill。由于包含的功能比较多,导致其体积较大(有2M之多)。
后来推出了core-js@3,使用 Monorepo 进行拆包,拆成了 5 个相关的包:
- core-js:是整个 core-js 的核心,提供了基础的垫片能力,但是直接使用 core-js 会污染全局命名空间和对象原型;
- core-js-pure:core-js-pure 提供了独立的命名空间,不污染全局变量;
- core-js-compact:根据 Browserslist 维护了不同宿主环境、不同版本下对应需要支持特性的集合;
- core-js-builder:结合 core-js-compact 以及 core-js,并利用 webpack 能力,根据需求打包出 core-js
- core-js-bundle
但是,如果我们在项目中直接安装 @babel/polyfill
,会看到如的警告提示:
@babel/polyfill 将被弃用,已经不再推荐使用了。那么该如何使用babel-polyfill的功能呢?
单独使用
如果不依赖前端构建工具单独使用的话,需要安装依赖 npm install --save core-js regenerator-runtime,然后需要在业务代码中需要进行引入:
import "core-js/stable";
import "regenerator-runtime/runtime";
import "core-js/stable";
import "regenerator-runtime/runtime";
注意:此时不再引入@babel/polyfill 这个包了,因为 @bable/polyfill 也是依赖 core-js 并且会锁死 2.x 版本。
配合webpack使用
更改 webpack 的配置文件中的 entry 配置:
// webpack.config.js
const path = require('path');
module.exports = {
entry: ['core-js/stable', 'regenerator-runtime/runtime', './main.js'],
output: {
filename: 'dist.js',
path: path.resolve(__dirname, '')
},
mode: 'development'
};
// webpack.config.js
const path = require('path');
module.exports = {
entry: ['core-js/stable', 'regenerator-runtime/runtime', './main.js'],
output: {
filename: 'dist.js',
path: path.resolve(__dirname, '')
},
mode: 'development'
};
@babel/preset-env
上面的解决方案中,是将垫片全量进行引入的,完整的 polyfills 文件非常大,不利于我们打包出来的体积和页面的性能。
下面我们 使用 Babel 的预设或者插件做到按需使用,也是现在项目开发中主流的使用方式,使用 @babel/preset-env
预设。
@babel/preset-env 这个预设包含所有标准的最新特性,转换那些已经被正式纳入 TC39 中的语法;该预设在 Babel6 的时候的名字是 babel-preset-env 在 Babel7 后,更名为 @babel/preset-env,该预设不只可以在编译时通过转换 AST 来进行语法转换,还有一个重要功能就是根据设置的参数针对性处理 polyfill,真正做到按需引入。
最基本的配置:
module.exports = {
presets: ["@babel/preset-env"],
plugins: []
}
module.exports = {
presets: ["@babel/preset-env"],
plugins: []
}
配置 targets 选项,支持 3个 版本的浏览器和 安卓4.4 以上的系统以及 iOS 9.0 以上的系统:
module.exports = {
presets: [["@babel/preset-env", {
targets: {
browsers: [
'last 3 versions',
'Android >= 4.4',
'iOS >= 9.0',
],
},
]],
plugins: []
}
module.exports = {
presets: [["@babel/preset-env", {
targets: {
browsers: [
'last 3 versions',
'Android >= 4.4',
'iOS >= 9.0',
],
},
]],
plugins: []
}
useBuiltIns
useBuiltIns 配置决定了 @babel/preset-env 该如何处理 polyfill。其选项值:"usage" 、"entry" 和 false, 默认为 false。
false
如果使用默认的 false,polyfill 就不会被按需处理会被全部引入
entry
设置为 entry,需要手动导入 @babel/polyfill,可以直接导入 core-js 和 regenerator-runtime 也可以在 webpack 的 entry 中设置。useBuiltIn: entry 的作用就是会自动将import "core-js/stable" 和 import "regenerator-runtime/runtime" 转换为目标环境的按需引入。
module.exports = {
presets: [["@babel/preset-env", {
useBuiltIns: "entry",
]],
plugins: []
}
module.exports = {
presets: [["@babel/preset-env", {
useBuiltIns: "entry",
]],
plugins: []
}
entry配置只针对目标环境,而不是具体代码,所以 Babel 会针对目标环境引入所有的 polyfill 扩展包,用不到的polyfill也可能会引入进来。所以,如果不需要考虑打包产物的大小,可以使用该配置。
usage
useBuiltIns 设置为 usage
,则不需要手动导入 polyfill,babel 检测出此配置会自动进行 polyfill 的引入。其配置如下:
module.exports = {
presets: [["@babel/preset-env", {
useBuiltIns: "usage",
]],
plugins: []
}
module.exports = {
presets: [["@babel/preset-env", {
useBuiltIns: "usage",
]],
plugins: []
}
usage 模式下,Babel 除了会针对目标环境引入 polyfill 的同时也会考虑项目代码代码中使用了哪些 ES6+ 的新特性,两者取一个最小的集合作为 polyfill 的导入。
所以,如果希望打包出的代码尽可能的精简,那么 usage
模式是一个不错的选择,并且这也是官方推荐的使用方式。
Babel 配置
@babel/preset-env 预设也可以让你自己选择需要使用 2 还是 3。并且这个参数只有 useBuiltIn 设置为 usage 或者 entry 时才会生效。
该配置默认值为 2,但是如果我们需要某些最新的 API 时,需要将其设置为 3。
@babel/runtime
@babel/runtime 是含有 babel 编译所需要的一些 helpers 函数。同时还提供了 regenerator-runtime,对 generator 和 async函数进行编译降级。
Babel 在转译 syntax 时,有时候会使用一些辅助的函数来帮忙,比如我们需要转译 class 类:
class 语法的转换过程中, @babel/preset-env 自定义了 _classCallCheck 这个函数来辅助转换。这个函数就是 helper 函数。这是 @babel/preset-env 在做语法转换的时候,注入了这些 helpers 函数声明,以便语法转换后使用。
helper 函数在转译后的文件中被定义了一遍。也就是说,项目中有多少个文件中存在需要转换的 class,那么在打包的产物中就会有多少个 _classCallCheck helper 函数,这显然不是一种高效的做法。
解决思路是将这些 helpers 函数都放入到某个依赖包中,在使用的时候直接从该包中引入即可,这样打包出来的产物中,就只有一份 helpers 函数。上面提到的 @babel/runtime 就是这个依赖包。
@babel/plugin-transform-runtime
@babel/plugin-transform-runtime 是帮我们用工程化的手段解决来解决问题的。我们使用 @babel/plugin-transform-runtime 自动将需要引入的 helpers 函数替换为 @babel/runtime 中的引用。
@babel/plugin-transform-runtime 还有另一个关键的作用就是对 API 进行转换的时候,避免污染全局变量。
@babel/polyfill 的处理机制是,对于例如 Array.from 等静态方法,直接在 global.Array 上添加;对于例如 includes 等实例方法,直接在 global.Array.prototype 上添加。
直接修改了全局变量的原型,造成全局污染的问题。在作为三方的插件使用,有可能引发冲突问题。
而 @babel/plugin-transform-runtime
将 Promise 转换为 _promise["default"],而 _promise["default"] 拥有ES标准里 Promise 所有的功能。现在,即使浏览器没有 Promise,我们的代码也能正常运行。
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var obj = _promise["default"].resolve();
var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");
var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));
var obj = _promise["default"].resolve();
因此,transform-runtime 插件的主要作用:
- 直接将 helpers 从文件中定义改为从 @babel/runtime 中引入,避免了多次引入 helpers 辅助函数。
- 将 @babel/ployfill 中 API 的 polyfill 直接修改原型改为从 @babel/runtime-corejs3/helpers中获取,避免对全局变量和原型的污染。
总结
本文简要介绍了 Babel 的作用,Babel的原理,Babel的插件,尤其是 @babel/preset-env
插件预设。 Babel的设计理念,如何处理polyfill,@babel/polyfill废弃的原因。如何使用 @babel/preset-env 结合 usage,生成项目所需的polyfill。 重点介绍了Babel在项目使用时的注意要点,尤其是如何合理配置polyfill,实现高质量打包代码的输出。